﻿using System;
using System.Collections.Generic;
using System.IO;
using Tao.OpenAl;

namespace HDM.NexusEngine.Core
{
    public class SoundManager : IDisposable
    {
        private const int MAX_SOUND_CHANNELS = 256;
        private float _masterVolume = 1.0f;
        private List<int> _soundChannels = new List<int>();
        private Dictionary<string, SoundSource> _soundIdentifier = new Dictionary<string, SoundSource>();

        public SoundManager()
        {
            Alut.alutInit();
            DiscoverSoundChannels();
        }

        public void LoadSound(string soundId, string path)
        {
            // Generate a buffer.
            int buffer;
            Al.alGenBuffers(1, out buffer);

            var errorCode = Al.alGetError();
            System.Diagnostics.Debug.Assert(errorCode == Al.AL_NO_ERROR);

            int format;
            float frequency;
            int size;
            System.Diagnostics.Debug.Assert(File.Exists(path));
            var data = Alut.alutLoadMemoryFromFile(path, out format, out size, out frequency);
            System.Diagnostics.Debug.Assert(data != IntPtr.Zero);

            // Load wav data into the generated buffer.
            Al.alBufferData(buffer, format, data, size, (int)frequency);

            // Everything seems OK, add it to the library.
            _soundIdentifier.Add(soundId, new SoundSource(buffer, path));
        }

        public Sound PlaySound(string soundId, bool loop = false)
        {
            var channel = FindNextFreeChannel();
            if (channel != -1)
            {
                Al.alSourceStop(channel);
                Al.alSourcei(channel, Al.AL_BUFFER, _soundIdentifier[soundId].BufferId);
                Al.alSourcef(channel, Al.AL_PITCH, 1.0f);
                Al.alSourcef(channel, Al.AL_GAIN, 1.0f);

                Al.alSourcei(channel, Al.AL_LOOPING, loop ? 1 : 0);

                Al.alSourcef(channel, Al.AL_GAIN, _masterVolume);
                Al.alSourcePlay(channel);
                return new Sound(channel);
            }
            else
            {
                // Error sound
                return new Sound(-1);
            }
        }

        public bool IsSoundPlaying(Sound sound)
        {
            return IsChannelPlaying(sound.Channel);
        }

        public void StopSound(Sound sound)
        {
            if (sound.Channel == -1) return;

            Al.alSourceStop(sound.Channel);
        }

        public void ChangeVolume(Sound sound, float value)
        {
            Al.alSourcef(sound.Channel, Al.AL_GAIN, _masterVolume * value);
        }

        public void MasterVolume(float value)
        {
            _masterVolume = value;
            foreach (var channel in _soundChannels)
            {
                Al.alSourcef(channel, Al.AL_GAIN, value);
            }
        }

        private bool IsChannelPlaying(int channel)
        {
            int value;
            Al.alGetSourcei(channel, Al.AL_SOURCE_STATE, out value);
            return (value == Al.AL_PLAYING);
        }

        private int FindNextFreeChannel()
        {
            foreach (var slot in _soundChannels)
            {
                if (!IsChannelPlaying(slot)) return slot;
            }

            return -1;
        }

        private void DiscoverSoundChannels()
        {
            while (_soundChannels.Count < MAX_SOUND_CHANNELS)
            {
                int src;
                Al.alGenSources(1, out src);
                if (Al.alGetError() == Al.AL_NO_ERROR)
                {
                    _soundChannels.Add(src);
                }

                // There's been an error - we've filled all the channels.
                break;
            }
        }

        public void Dispose()
        {
            foreach (var soundSource in _soundIdentifier.Values)
            {
                var temp = soundSource;
                Al.alDeleteBuffers(1, ref temp.BufferId);
            }

            _soundIdentifier.Clear();
            foreach (var slot in _soundChannels)
            {
                var target = _soundChannels[slot];
                Al.alDeleteSources(1, ref target);
            }

            Alut.alutExit();
        }

        struct SoundSource
        {
            public int BufferId;
            private string _filePath;
            
            public SoundSource(int bufferId, string filePath)
            {
                BufferId = bufferId;
                _filePath = filePath;
            }
        }
    }
}